namespace Game {
    using System.Collections.Generic;

    using UnityEngine;
    using UnityEngine.Events;
    using UnityEngine.SceneManagement;

    public class Cache : MonoBehaviour {
        public enum Mode {
            Temp5s,
            WithScene,
            WithApplication
        }

        public class Event : UnityEvent<Object> {}

        public Event onUnload = new Event();

        public void Add(string key, Object obj, Mode mode = Mode.Temp5s) {
            if (contents.ContainsKey(key)) {
                Logger.Warn("Duplicated key: {0} in cache", key);
                Del(key);
            }

            Content content = new Content();
            content.Obj = obj;
            content.UnloadMode = mode;
            content.Expire = (mode == Mode.Temp5s ? (Time.realtimeSinceStartup + 5.0f) : -1.0f);
            contents.Add(key, content);
        }

        public T Get<T>(string key) where T : Object {
            Content content = null;
            if (contents.TryGetValue(key, out content)) {
                if (content.UnloadMode == Mode.Temp5s) content.Expire += 5.0f;
                return content.Obj as T;
            }
            return null;
        }

        public void Del(string key) {
            Content content = null;
            if (contents.TryGetValue(key, out content)) {
                onUnload.Invoke(content.Obj);
                contents.Remove(key);
            }
        }

        private void Awake() {
            SceneManager.sceneUnloaded += OnUnloadScene;
        }

        private void OnDestroy() {
            SceneManager.sceneUnloaded -= OnUnloadScene;
            foreach (var kv in contents) onUnload.Invoke(kv.Value.Obj); contents.Clear();
        }

        private void FixedUpdate() {
            var now = Time.realtimeSinceStartup;

            delay.Clear();
            foreach (var kv in contents) {
                if (kv.Value.UnloadMode == Mode.Temp5s && kv.Value.Expire <= now) {
                    onUnload.Invoke(kv.Value.Obj); delay.Add(kv.Key);
                }
            }

            foreach (var key in delay) contents.Remove(key);
        }

        private void OnUnloadScene(Scene _) {
            var cur = SceneManager.GetActiveScene().buildIndex;

            delay.Clear();
            foreach (var kv in contents) {
                var content = kv.Value;
                
                if (content.UnloadMode == Mode.Temp5s ||
                    content.UnloadMode == Mode.WithScene && content.Scene != cur) {
                    onUnload.Invoke(content.Obj);
                    delay.Add(kv.Key);
                }
            }

            foreach (var key in delay) contents.Remove(key);
        }

        private class Content {
            public Object   Obj = null;
            public Mode     UnloadMode = Mode.Temp5s;
            public float    Expire = 0.0f;
            public int      Scene = 0;
        }

        private Dictionary<string, Content> contents = new Dictionary<string, Content>();

        private List<string> delay = new List<string>();
    }
}